package Gtk2::Ex::ButtonPathBar;

our $VERSION = '0.02';

use strict;
use Gtk2;
use Glib::Object::Subclass
	Gtk2::Box::,
	signals => {
		size_request => \&do_size_request,
		size_allocate => \&do_size_allocate,
	};

=head1 NAME

Gtk2::Ex::ButtonPathBar - A button path bar widget

=head1 SYNOPSIS

	use Gtk2;
	use Gtk2::Ex::ButtonPathBar;
	use File::Spec;
	
	my $pathbar = Gtk2::Ex::ButtonPathBar->new(spacing => 3);
	$pathbar->show;
	
	# show a path
	my $name = '/foo/bar/baz';
	my ($vol, $dir, $file) = File::Spec->splitpath($name);
	my @path = (($vol || '/'), File::Spec->splitdir($dir), $file);
	$pathbar->set_path(\&load_dir, grep length($_), @path);

	# or show a history
	my @hist = qw(/foo/bar /tmp/baz /home/pardus);
	$pathbar->set_path(\&load_dir, map [basename($_), $_], @hist);

	sub load_dir {
		my ($button, @path) = @_;
		# ...
	}

=head1 DESCRIPTION

This widget is intended as a look-a-like for the "button path bar" used
in the gtk file dialog. Each part of the path is represented by a button.
If the button row gets to long sliders are shown left and right to scroll
the bar.

It can be used to display a path to a file or directory or to display
similar data like a history (trace) or a namespace.

=head1 HIERARCHY

  Glib::Object
  +----Gtk2::Object
        +----Gtk2::Widget
              +----Gtk2::Container
	           +----Gtk2::Box
                        +----Gtk2::Ex::ButtonPathBar

=head1 METHODS

=over 4

=item C<< new(spacing => $spacing, ..) >>

Simple constructor, takes pairs of properties.

=cut

# constructor inherited from Glib::Object

sub INIT_INSTANCE {
	my $self = shift;
	$self->{anchor} = ['left_item', 0];
	$self->{_scroll_timeout} = -1;
	
	my $l_slider = Gtk2::Button->new();
	my $l_arrow = Gtk2::Arrow->new('left', 'out');
	$l_slider->add($l_arrow);
	$l_slider->signal_connect( button_press_event =>
		\&on_button_press_event, [$self, 'left']);
	$l_slider->signal_connect( button_release_event =>
		\&on_button_release_event, [$self, 'left']);
	$l_slider->show_all;
	$self->add($l_slider);
	my ($x, $y) = $l_arrow->get_padding;
	
	my $r_slider = Gtk2::Button->new();
	my $r_arrow = Gtk2::Arrow->new('right', 'out');
	$r_slider->add($r_arrow);
	$r_slider->signal_connect( button_press_event =>
		\&on_button_press_event, [$self, 'right']);
	$r_slider->signal_connect( button_release_event =>
		\&on_button_release_event, [$self, 'right']);
	$r_slider->show_all;
	$self->add($r_slider);
}

sub on_button_press_event {
	my (undef, $event) = @_;
	my ($self, $direction) = @{$_[2]};
	return unless $event->button == 1;
	
	my $timeout = $self->{_scroll_timeout};
	Glib::Source->remove($timeout) if $timeout >= 0;
	$self->{_scrolled} = 0;
	$self->{_scroll_timeout} = Glib::Timeout->add(250, sub {
		$self->{_scrolled} = 1;
		($direction eq 'left')
			? $self->scroll_left(1)
			: $self->scroll_right(1) ;
	} );
}

sub on_button_release_event {
	my ($self, $direction) = @{$_[2]};
	my $timeout = $self->{_scroll_timeout};
	Glib::Source->remove($timeout) if $timeout >= 0;
	$self->{_scroll_timeout} = -1;

	unless ($self->{_scrolled}) {
		($direction eq 'left')
			? $self->scroll_left(1)
			: $self->scroll_right(1) ;
	}
}

sub do_size_request {
	# get minimum required size and store it in $requisition
	my ($self, $requisition) = @_;
	my $spacing = $self->get_spacing;
	my $border = $self->get_border_width;
	
	my ($max_width, $max_height, $slider_width);
	my $i = 0;
	for ($self->get_children) {
		my $c_requisition = $_->size_request;
		my ($w, $h) = ($c_requisition->width, $c_requisition->height);
		$max_height = $h if $h > $max_height;
		if (++$i < 3) { $slider_width += $w                }
		else          { $max_width = $w if $w > $max_width }
	}
	
	$max_width += $slider_width + 2 * $spacing + 2 * $border;
	$max_height += 2 * $border;
	$requisition->height($max_height);
	$requisition->width($max_width);
}

# In order to display as many items as possible we use the following
# scrolling algorith:
#
# There is an "anchor" containing "left_item" or "right_item" plus an
# item index. The anchor item will be visible. We start filling the space
# by taking this anchor as either left or right side of the visible range
# and start adding other items from there. We can run out of items when we
# reach the end of the item list, or we can run out of space when the next
# item is larger than the space that is left.
# Next we check if we can show more items by adding items in the opposite
# direction. Possibly replacing our anchor as outer left or outer right item.
# Once we know which items are the real "left_item" and "right_item" we 
# start allocating space to the widgets.
#
# Slide buttons are shown when all buttons do not fit in the given space.
# The space is calculated either with or without sliders. At the end we
# check if the sliders are really needed. We choose to hide the slider if
# it can't be used to scroll more instead of making it insensitive because 
# the arrows do not show very clear when they are sensitive and when not.
# The space that is freed when a slider is hidden is not recycled because
# that would pose the risk of clicking on a button when the slider suddenly
# disappears.

sub do_size_allocate {
	my ($self, $allocation) = @_;
	my ($l_slider, $r_slider, @items) = $self->get_children;
	return unless @items; # no items to allocate
	
	# get basic parameters
	my ($x, $y, $w, $h) = ($allocation->values);
	my $spacing = $self->get_spacing;
	my $border = $self->get_border_width;
	my @items_width = map $_->requisition->width, @items;
	my $l_width = $l_slider->get_child->requisition->width; # width of arrow
	my $r_width = $r_slider->get_child->requisition->width;
	my ($anchor, $first_item) = @{$self->{anchor}};
	#print "anchor: $anchor, $first_item\n";

	# calculate which items to display
	my $total_width = $spacing * $#items;
	$total_width += $_ for @items_width;
	my $show_sliders = ($total_width > $w - 2 * $border);
	my ($show_l_slider, $show_r_slider) = ($show_sliders, $show_sliders);
	my $available_width = $w - 2 * $border;
	$available_width -= $l_width + $r_width + 2 * $spacing if $show_sliders;
	$available_width -= $items_width[$first_item];
	my $last_item = $first_item;
	for ( ($anchor eq 'left_item')
		? ($first_item + 1 .. $#items  )
		: (reverse 0 .. $first_item - 1)
	) {
		if ($available_width > $spacing + $items_width[$_]) {
			$available_width -= $spacing + $items_width[$_];
			$last_item = $_;
		}
		else { last }
	}
	#print "available: $available_width, first and last: $first_item, $last_item, show_sliders: $show_sliders\n";
	
	# check if we have any space left to fill from the other side
	for ( ($anchor eq 'left_item')
		? (reverse 0 .. $first_item - 1)
		: ($first_item + 1 .. $#items  )
	) {
		if ($available_width > $spacing + $items_width[$_]) {
			$available_width -= $spacing + $items_width[$_];
			$first_item = ($anchor eq 'left_item')
				? $first_item - 1 : $first_item + 1 ;
		}
		else { last }
	}
	
	# allocate items
	my $child_allocation = Gtk2::Gdk::Rectangle->new(
		0, $y + $border, 0, $h - 2 * $border); # x, y, w, h
	my ($left_item, $right_item) = ($anchor eq 'left_item')
		? ($first_item, $last_item) : ($last_item, $first_item) ;
	@{$self}{qw/left_item right_item/} = ($left_item, $right_item);
	#print "left and right: $left_item, $right_item\n";
	# my $dir = $self->get_direction;
	my $child_x = $x + $border;
	$child_x += $l_width + $spacing if $show_sliders;
	for ($left_item .. $right_item) {
		#print "allocating item $_\n";
		$child_allocation->width($items_width[$_]);
		$child_allocation->x($child_x);
		$items[$_]->set_child_visible(1);
		$items[$_]->size_allocate($child_allocation);
		$child_x += $items_width[$_] + $spacing;
	}
	
	# hide invisible items
	$items[$_]->set_child_visible(0)
		for 0 .. $left_item - 1, $right_item + 1 .. $#items;
	
	# allocate sliders
	if ($left_item != 0) {
		$child_allocation->width($l_width);
		$child_allocation->x($x + $border);
		$l_slider->set_child_visible(1);
		$l_slider->size_allocate($child_allocation);
	}
	else {	$l_slider->set_child_visible(0) }
	
	if ($right_item != $#items) {
		$child_allocation->width($r_width);
		$child_allocation->x($x + $w - $border - $r_width);
		$r_slider->set_child_visible(1);
		$r_slider->size_allocate($child_allocation);
	}
	else {	$r_slider->set_child_visible(0) }

	# Thats all ...
}

=item C<scroll_left(INT)>

Scrolls the path bar one or more items to the left.

=cut

sub scroll_left {
	my $self = shift;
	shift if ref $_[0];
	my $i = shift || 1;
	my $l_item = $self->{left_item};
	return 0 if $l_item == 0;
	$l_item = ($l_item < $i) ? 0 : $l_item - $i;
	$self->{anchor} = ['left_item', $l_item];
	$self->queue_resize;
	return 1;
}

=item C<scroll_right(INT)>

Scrolls the path bar one or more items to the right.

=cut

sub scroll_right {
	my $self = shift;
	shift if ref $_[0];
	my $i = shift || 1;
	my $r_item = $self->{right_item};
	my (undef, undef, @items) = $self->get_children;
	return 0 if $r_item == $#items;
	$r_item = ($r_item+$i > $#items) ? $#items : $r_item+$i;
	$self->{anchor} = ['right_item', $r_item];
	$self->queue_resize;
	return 1;
}

=item C<set_path(CODE, PART, PART, ..)>

This method fills the path bar with a button for each part
of the path. When a button is clicked the code reference 
is called with as argument a list representing the path to that button.

If a part is an array ref, the first item in this array is displayed on the
button and the remaining items are passed as arguments to the code ref instead of
the path. This can be used if the data you want to display is not really a path.

=cut

sub set_path {
	my ($self, $code, @parts) = @_;
	my @buttons;
	my @path;
	for (@parts) {
		my ($name, @arg);
		if (ref $_) {
			($name, @arg) = @$_;
		}
		else {
			$name = $_;
			push @path, $_;
			@arg = @path; # copy
		}
		my $button = Gtk2::ToggleButton->new_with_label($name);
		$button->{path_data} = \@arg;
		$button->signal_connect_swapped(clicked => \&_button_clicked, $self);
		$button->show;
		push @buttons, $button;
	}
	$self->set_items(@buttons);
	$self->{button_clicked_handler} = $code;
}

sub _button_clicked {
	my ($self, $button) = @_;
	$self->select_item($button);
	$self->{button_clicked_handler}->($button, @{$button->{path_data}});
}

=item C<set_items(WIDGET, WIDGET, ..)>

Low level method used by C<set_path()> to fill the path bar with buttons.
This can be used to fill the path bar with your own buttons or other widgets.

=cut

sub set_items {
	my $self = shift;
	$self->clear_items;
	$self->pack_start($_, 0,0,0) for @_;
	$self->select_item($#_);
}

=item C<get_items()>

Returns a list of widgets coresponding to the items in the path bar.
Notice that this differs from C<get_children()> which will also give
you the widgets for the sliders.

=cut

sub get_items {
	my $self = shift;
	my (undef, undef, @items) = $self->get_children;
	return @items;
}

=item C<clear_items()>

Removes all buttons from the path bar.

=cut

sub clear_items { # reset all rendering attributes
	my $self = shift;
	$self->{anchor} = [0, 'left_item'];
	$self->{left_item} = -1;
	$self->{right_item} = -1;
	$self->{selected_item} = undef;
	$self->{button_clicked_handler} = undef;
	my (undef, undef, @items) = $self->get_children;
	$self->remove($_) for @items;
}

=item C<select_item(INDEX)>

Selects the button associated with path part number INDEX in the array given
to C<set_path()> or C<set_items()>. This will have the same visual effect as
when the user clicked the button in question.

If you used C<set_items()> to add your own widgets you can also use an object
ref instead of INDEX here.

=cut

sub select_item {
	# toggle selected item, untoggle previous selected item
	# selected item gets bold text
	my ($self, $item) = @_;
	my (undef, undef, @items) = $self->get_children;
	
	my $s_item = $self->{selected_item};
	if ($s_item and $s_item->isa('Gtk2::ToggleButton')) {
		$s_item->signal_handlers_block_by_func(\&_button_clicked);
		$s_item->set_active(0);
		my $label = $s_item->get_child;
		if ($label->isa('Gtk2::Label')) { # remove bold
			my $text = $label->get_text;
			$label->set_text($text);
		}
		$s_item->signal_handlers_unblock_by_func(\&_button_clicked);
	}
	
	$item = $items[$item] unless ref $item; # allow for index and object ref
	if ($item->isa('Gtk2::ToggleButton')) {
		$item->signal_handlers_block_by_func(\&_button_clicked);
		$item->set_active(1);
		my $label = $item->get_child;
		if ($label->isa('Gtk2::Label')) { # set text bold
				my $text = $label->get_text;
				$label->set_markup("<b>$text</b>");
		}
		$item->signal_handlers_unblock_by_func(\&_button_clicked);
	}
	$self->{selected_item} = $item;

	# bring/keep selected item in the visual range
	$self->queue_resize;
	my ($i) = grep {$items[$_] eq $item} 0 .. $#items;
	my $anchor =	($i < $self->{left_item})  ? 'left_item'  :
			($i > $self->{right_item}) ? 'right_item' : undef;
	if ($anchor) {
		$self->{anchor} = [$anchor, $i];
		$self->queue_resize;
	}
}

1;

__END__

=back

=head1 AUTHOR

Jaap Karssenberg || Pardus [Larus] <pardus@cpan.org>

Copyright (c) 2005 Jaap G Karssenberg. All rights reserved.  This program
is free software; you can redistribute it and/or modify it under the same
terms as Perl itself.

=cut

